/*
     Copyright 2007 Joseph M. Ferner

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using log4net;
using OpenServers.Net.Common.FileSystem;
using OpenServers.Net.Common.Listener;
using OpenServers.Net.Common.Security;
using OpenServers.Net.Common.Socket;
using OpenServers.Net.Common.State;
using OpenServers.Net.FtpServer.Command;
using OpenServers.Net.FtpServer.State;
using OpenServers.Net.Common;

namespace OpenServers.Net.FtpServer {
    /// <summary>
    /// A FTP server.
    /// </summary>
    public class FtpServer : AbstractServer, IFtpServer {
        private static readonly ILog log = LogManager.GetLogger(typeof(FtpServer));

        /// <summary>
        /// The default port to listen on for nonsecure connections.
        /// </summary>
        public const int DEFAULT_NONSECURE_PORT = 21;

        private readonly IDictionary<string, CommandContainer> _commandMap = new Dictionary<string, CommandContainer>();
        private ISecurityProvider _securityProvider;
        private IFileSystem _fileSystem;
        private readonly IMessages _messages = new Messages();

        /// <summary>
        /// Constructor.
        /// </summary>
        public FtpServer() {
            CommandMap.Add("CWD", CreateCommandContainer(typeof(ChangeWorkingDirectoryCommand)));
            CommandMap.Add("DELE", CreateCommandContainer(typeof(DeleteCommand)));
            CommandMap.Add("FEAT", CreateCommandContainer(typeof(FeatureCommand)));
            CommandMap.Add("LIST", CreateCommandContainer(typeof(ListCommand)));
            CommandMap.Add("MKD", CreateCommandContainer(typeof(MakeDirectoryCommand)));
            CommandMap.Add("NLST", CreateCommandContainer(typeof(NameListCommand)));
            CommandMap.Add("NOOP", CreateCommandContainer(typeof(NoOpCommand)));
            CommandMap.Add("PASS", CreateCommandContainer(typeof(PasswordCommand)));
            CommandMap.Add("PORT", CreateCommandContainer(typeof(PortCommand)));
            CommandMap.Add("PWD", CreateCommandContainer(typeof(PrintWorkingDirectoryCommand)));
            CommandMap.Add("REST", CreateCommandContainer(typeof(RestartCommand)));
            CommandMap.Add("RETR", CreateCommandContainer(typeof(RetrieveCommand)));
            CommandMap.Add("RMD", CreateCommandContainer(typeof(DeleteDirectoryCommand)));
            CommandMap.Add("RNFR", CreateCommandContainer(typeof(RenameFromCommand)));
            CommandMap.Add("RNTO", CreateCommandContainer(typeof(RenameToCommand)));
            CommandMap.Add("STOR", CreateCommandContainer(typeof(StoreCommand)));
            CommandMap.Add("SYST", CreateCommandContainer(typeof(SystemCommand)));
            CommandMap.Add("TYPE", CreateCommandContainer(typeof(TypeCommand)));
            CommandMap.Add("USER", CreateCommandContainer(typeof(UserCommand)));
            CommandMap.Add("QUIT", CreateCommandContainer(typeof(QuitCommand)));

            CommandMap.Add("XMKD", CreateCommandContainer(typeof(MakeDirectoryCommand)));
            CommandMap.Add("XPWD", CreateCommandContainer(typeof(PrintWorkingDirectoryCommand)));
            CommandMap.Add("XRMD", CreateCommandContainer(typeof(DeleteDirectoryCommand)));

            InitializeState += FtpInitializeState;
        }

        /// <summary>
        /// The security provider.
        /// </summary>
        public ISecurityProvider SecurityProvider {
            get { return _securityProvider; }
            set { _securityProvider = value; }
        }

        /// <summary>
        /// The backing file system.
        /// </summary>
        public IFileSystem FileSystem {
            get { return _fileSystem; }
            set { _fileSystem = value; }
        }

        /// <summary>
        /// The system type string <see cref="SystemCommand"/>
        /// </summary>
        public string SystemTypeString {
            get { return "WIN32"; }
        }

        /// <summary>
        /// A map of all the valid FTP commands.
        /// </summary>
        public IDictionary<string, CommandContainer> CommandMap {
            get { return _commandMap; }
        }

        /// <summary>
        /// the messages provider.
        /// </summary>
        public IMessages Messages {
            get { return _messages; }
        }

        private static CommandContainer CreateCommandContainer(Type commandType) {
            if (commandType.IsAssignableFrom(typeof(BaseCommand))) {
                throw new ApplicationException(string.Format("invalid command type [{0}], you must extend from [{1}]", commandType.FullName, typeof(BaseCommand).FullName));
            }
            ConstructorInfo constructor = commandType.GetConstructor(new Type[] { });
            return new CommandContainer(commandType, constructor);
        }

        /// <summary>
        /// Creates a state object.
        /// </summary>
        /// <param name="listener">the listener who called this method.</param>
        /// <param name="clientSocket">The client socket.</param>
        /// <returns></returns>
        public override IState CreateState(IListener listener, ISocket clientSocket) {
            return new FtpState(listener, clientSocket);
        }

        private void FtpInitializeState(IServer server, IState state) {
            state.DataReceived += DataRecieved;
            state.ConnectionOpened += _ConnectionOpened;
            state.ConnectionClosed += _ConnectionClosed;
            state.AttachThread += AttachThread;
            state.DetachThread += DetachThread;
        }

        /// <summary>
        /// called when a state is detached from a thread.
        /// </summary>
        protected virtual void DetachThread(IState state, Thread thread) {
        }

        /// <summary>
        /// called when a state is attached from a thread.
        /// </summary>
        protected virtual void AttachThread(IState state, Thread thread) {
        }

        private void _ConnectionClosed(IState stateArg) {
            FtpState state = (FtpState)stateArg;
            ConnectionClosed(state);
        }

        /// <summary>
        /// called when a connection is closed.
        /// </summary>
        protected virtual void ConnectionClosed(FtpState state) {

        }

        private void _ConnectionOpened(IState stateArg) {
            FtpState state = (FtpState)stateArg;
            ConnectionOpened(state);
            state.Listener.Send(state, Messages.Get220());
        }

        /// <summary>
        /// called when a connection is opened.
        /// </summary>
        protected virtual void ConnectionOpened(FtpState state) {
        }

        private void DataRecieved(IState stateArg, byte[] buffer, int bytesRead) {
            FtpState state = (FtpState)stateArg;
            if (bytesRead > 0) {
                for (int i = 0; i < bytesRead; i++) {
                    char ch = (char)buffer[i];
                    if (ch == '\n') {
                        string command = state.CommandBuffer.ToString();
                        ProcessCommand(state, command);
                        state.CommandBuffer.Remove(0, state.CommandBuffer.Length);
                    } else {
                        state.CommandBuffer.Append(ch);
                    }
                }
            }
        }

        private void ProcessCommand(FtpState state, string command) {
            command = command.TrimEnd('\r', ' ', '\t', '\f');
            if (command.Length > 0) {
                int commandTermToken = command.IndexOf(' ');
                string commandStr;
                string arguments;
                if (commandTermToken > 0) {
                    commandStr = command.Substring(0, commandTermToken).Trim().ToUpper();
                    arguments = command.Substring(commandTermToken + 1).Trim();
                } else {
                    commandStr = command;
                    arguments = "";
                }
                ProcessCommand(state, commandStr, arguments);
            }
        }

        private void ProcessCommand(FtpState state, string command, string arguments) {
            log.DebugFormat("command received [{0}] with arguments [{1}]", command, arguments);
            if (CommandMap.ContainsKey(command)) {
                ConstructorInfo commandConstructor = CommandMap[command].CommandConstructor;
                BaseCommand cmd = (BaseCommand)commandConstructor.Invoke(new object[] { });
                cmd.Invoke(this, state, arguments);
            } else {
                log.WarnFormat("invalid command [{0}]", command);
                state.Listener.Send(state, Messages.Get502());
            }
        }

        /// <summary>
        /// container for commands.
        /// </summary>
        public class CommandContainer {
            private readonly Type _commandType;
            private readonly ConstructorInfo _commandConstructor;

            /// <summary>
            /// constructor.
            /// </summary>
            public CommandContainer(Type commandType, ConstructorInfo commandConstructor) {
                _commandType = commandType;
                _commandConstructor = commandConstructor;
            }

            /// <summary>
            /// The command type.
            /// </summary>
            public Type CommandType {
                get { return _commandType; }
            }

            /// <summary>
            /// The constructor for the command object.
            /// </summary>
            public ConstructorInfo CommandConstructor {
                get { return _commandConstructor; }
            }
        }
    }
}
